class Issue < ActiveRecord::Base
  belongs_to :submitter,
              :class_name => 'User',
              :foreign_key => :submitter_id,
              :counter_cache => :submitted_issues_count

  belongs_to :status
  belongs_to :priority

  belongs_to :project, :counter_cache => true

  has_many :adoptions
  has_many :owners, :through => :adoptions, :source => :user

  has_many :subscriptions
  has_many :subscribers, :through => :subscriptions, :source => :user

  has_many :comments

  has_many :attachment_links, :as => :attachable
  has_many :attachments, :through => :attachment_links

  named_scope :open,
                :joins => :status,
                :conditions => ['statuses.closed = ?', false],
                :order => 'issues.created_at ASC'
  named_scope :closed,
                :joins => :status,
                :conditions => ['statuses.closed = ?', true]
  named_scope :recent,
                :conditions => [
                    'issues.updated_at > ?',
                    Time.now - 7.days
                  ],
                :order => 'issues.updated_at ASC'

  acts_as_taggable
  acts_as_commentable

  # all fields are required
  validates_presence_of :summary
  validates_presence_of :details
  validates_presence_of :submitter_id
  validates_presence_of :project_id
  validates_presence_of :local_id     # set automatically if left nil
  validates_presence_of :status_id

  # issue numbering must be unique for each project
  validates_uniqueness_of :local_id, :scope => :project_id

  # fix the attachments error messages
  validates_each :attachments do |record, attr, value|
    value.each do |a|
      unless a.valid?
        # this code just adds the individual errors:
        # a.errors.each do |error_name, error_value|
        #   record.errors.add( error_name, error_value )
        # end
        if a.is_a? IssueRef
          record.errors.add( "Issue attachment", 'requries a project name and an issue number' )
        elsif a.is_a? CommentRef
          record.errors.add( "Comment attachment (\"#{a.description}\")", 'requries a comment number' )
        elsif a.is_a? Image
          record.errors.add( "Image attachment (\"#{a.description}\")", 'is missing a file' )
        elsif a.is_a? PatchFile
          record.errors.add( "Patch file attachment (\"#{a.description}\")", 'is missing a file' )
        elsif a.is_a? GenericFile
          record.errors.add( "File attachment (\"#{a.description}\")", 'is missing a file' )
        end
      end
    end
    # remove any automatically inserted attachments errors
    record.errors.clear_on( 'attachments' )
  end

  # return address for e-mail posts
  def reply_to
    "project_#{project.id}_issue_#{local_id}"
  end

  # returns true if this issue open
  def open?
    not self.status.closed
  end
  
  # returns true if this issue is closed
  def closed?
    self.status.closed
  end

  # before saving to the DB, get a unique local_id, mark as 'new'
  def before_validation_on_create
    self.status_id ||= Status.find_by_name( 'new' )
    self.local_id = 1 + (project.issues.maximum('local_id') || 0 )
  end

  # opens a new issue for project
  def self.open_issue_for( project, user, summary, details )
    create( :project_id => project.id, :submitter_id => user.id,
            :summary => summary, :details => details )
  end

  # returns the correct issue using project-local numbering
  def self.find_project_local( project, local_id, options = {} )
    # local_id needs to be a number
    local_id = local_id.to_i

    if project.is_a? Project
      Issue.find( :first, :conditions =>
          ['project_id = ? AND local_id = ?', project.id, local_id],
          :include => options[:include] )
    elsif project.is_a? String
      if project =~ /.*\D.*/
        Issue.find( :first,
            :joins => 'JOIN `projects` AS p ON p.id = project_id',
            :conditions => ['p.name = ? AND local_id = ?',
                            project, local_id],
            :include => options[:include] )
      else
        Issue.find( :first, :conditions =>
            ['project_id = ? AND local_id = ?', project, local_id],
            :include => options[:include] )
      end
    elsif project.is_a? Numeric
      Issue.find( :first, :conditions =>
          ['project_id = ? AND local_id = ?', project, local_id],
          :include => options[:include] )
    end
  end

  # returns all of the open issues for project or user
  def self.find_open_for( project_or_user )
    if project_or_user.is_a? User
      find( :all, :joins => 'JOIN adoptions AS a ON a.issue_id = issues.id ' +
                            'JOIN statuses AS s ON s.id = issues.status_id',
            :conditions => ['(submitter_id = ? OR ' +
                            '( a.user_id = ? AND a.disowned = ? ) ) AND ' +
                            's.closed = ?',
                            project_or_user.id, project_or_user.id, false, false] )
    elsif project_or_user.is_a? Project
      find( :all, :joins => 'JOIN statuses AS s ON s.id = issues.status_id',
            :conditions => ["project_id = ? AND s.closed = ?",
                             project_or_user.id, false] )
    else
      raise ArgumentError, 'find_open_for must be given a Project or a User'
    end
  end

  # returns all of the closed issues for project or user
  def self.find_closed_for( project_or_user )
    if project_or_user.is_a? User
      find( :all, :joins => 'JOIN adoptions AS a ON a.issue_id = issues.id ' +
                            'JOIN statuses AS s ON s.id = issues.status_id',
            :conditions => ['(submitter_id = ? OR ' +
                            '( a.user_id = ? AND a.disowned = ? ) ) AND ' +
                            's.closed = ?',
                            project_or_user.id, project_or_user.id, false, true] )
    elsif project_or_user.is_a? Project
      find( :all, :joins => 'JOIN statuses AS s ON s.id = issues.status_id',
            :conditions => ["project_id = ? AND s.closed = ?",
                             project_or_user.id, true] )
    else
      raise ArgumentError, 'find_open_for must be given a Project or a User'
    end
  end

  def comment_attachments
   return AttachmentLink.find_attachments_for( comments )
  end

  # returns the total number of positive votes minus the total number of
  # negative votes for the issue
  def popularity
    Vote.popularity_of( self )
  end

  # returns an array of IssueChanges
  def changes( attributes = {} )
    return [] unless attributes

    changes = []

    ##### status
    s = nil
    if attributes.key? :status_id
      s = Status.find( attributes[:status_id] )
    elsif attributes.key? :status
      if attributes[:status].is_a? String
        s = Status.find_or_create_by_name( attributes[:status] )
        # store it so we can use this hash in #update_attributes
        attributes[:status] = s
      elsif attributes[:status].is_a? Status
        s = params[:status]
      end
    end

    changes << IssueChange.record(
        :type => :ref,
        :from => self.status,
        :to => s
      ) unless ( s.nil? or self.status == s )

    ##### priority
    p = nil
    if attributes.key? :priority_id
      p = Priority.find( attributes[:priority_id] )
    elsif attributes.key? :priority
      if attributes[:priority].is_a? String
        p = Priority.find_or_create_by_name( attributes[:priority] )
        # store it so we can use this hash in #update_attributes
        attributes[:priority] = p
      elsif attributes[:priority].is_a? Priority
        p = params[:priority]
      end
    end

    changes << IssueChange.record(
        :type => :ref,
        :from => self.priority,
        :to => p
      ) unless ( p.nil? or self.priority == p )

    ##### tags
    if attributes.key? :tag_list
      tl = TagList.new( attributes[:tag_list], :parse => true ).to_set
      removed = self.tag_list.to_set - tl
      added = tl - self.tag_list.to_set

      if removed.size > 0 or added.size > 0
        changes << IssueChange.record(
            :type => :attr,
            :attr => 'tags',
            :from => removed.to_a.join(', '),
            :to => added.to_a.join(', ')
          )
      end
    end

    ##### percent complete
    if ( attributes.key? :percent_complete and
         attributes[:percent_complete] != self.percent_complete )
      changes << IssueChange.record(
          :type => :attr,
          :attr => 'percent_complete',
          :from => self.percent_complete,
          :to => attributes[:percent_complete]
        )
    end

    ##### summary/details
    if ( attributes.key? :summary and
         attributes[:summary] != self.summary )
      changes << IssueChange.record(
          :type => :attr,
          :attr => 'summary',
          :from => self.summary,
          :to => attributes[:summary]
        )
    end
    if ( attributes.key? :details and
         attributes[:details] != self.details )
      changes << IssueChange.record(
          :type => :attr,
          :attr => 'details',
          :from => self.details,
          :to => attributes[:details]
        )
    end

    changes
  end

  def inspect
    "Issue( #{project ? project.name : ''} ##{local_id}, #{status}, #{owners.size} owners, #{comments.size} comments, #{attachments.size} attachments )"
  end

end
